/*
 * Copyright (c) 2015. For Intelligent Group.
 */

package com.intelligent.ispc.core.service.impl;

import com.google.common.collect.Lists;
import com.intelligent.ispc.common.mapper.BeanMapper;
import com.intelligent.ispc.common.mapper.BeanUtil;
import com.intelligent.ispc.common.persistence.DynamicSpecifications;
import com.intelligent.ispc.common.persistence.Hibernates;
import com.intelligent.ispc.common.persistence.SearchFilter;
import com.intelligent.ispc.common.utils.ApplicationException;
import com.intelligent.ispc.common.utils.Digests;
import com.intelligent.ispc.common.utils.Encodes;
import com.intelligent.ispc.common.utils.ExceptionMessages;
import com.intelligent.ispc.core.dto.RoleDto;
import com.intelligent.ispc.core.dto.UserDto;
import com.intelligent.ispc.core.entity.Organization;
import com.intelligent.ispc.core.entity.Role;
import com.intelligent.ispc.core.entity.User;
import com.intelligent.ispc.core.repository.jpa.RoleDao;
import com.intelligent.ispc.core.repository.jpa.UserDao;
import com.intelligent.ispc.core.service.SecurityService;
import com.intelligent.ispc.core.service.UserService;
import com.intelligent.ispc.foundation.mail.MailDto;
import com.intelligent.ispc.mail.MimeMailServiceImpl;
import com.intelligent.ispc.utils.Constant;
import com.intelligent.ispc.utils.SystemConfig;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.codec.CodecException;
import org.apache.shiro.util.ByteSource;
import org.dozer.DozerBeanMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.*;
import java.io.File;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * Created by Rovey.Luo on 2015/10/9.
 */
@Service
class UserServiceImpl implements UserService {
    private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    private static int getRandom(int count) {
        return (int) Math.round(Math.random() * (count));
    }

    private static String string = "abcdefghijklmnopqrstuvwxyz123456789";

    private static String getRandomString(int length) {
        StringBuffer sb = new StringBuffer();
        int len = string.length();
        for (int i = 0; i < length; i++) {
            sb.append(string.charAt(getRandom(len - 1)));
        }
        return sb.toString();
    }

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;

    @Autowired
    private SystemConfig systemConfig;

    @Autowired
    private com.intelligent.ispc.core.repository.jdbc.UserDao userDaoJdbc;

    @Autowired
    private MimeMailServiceImpl mimeMailService;


    @Override
    public void saveUser(UserDto userDto, String password) {
        if (userDto == null || userDto.getLoginName() == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        User user = userDao.findByLoginName(userDto.getLoginName());
        if (user != null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_ALREADY_EXISTS);
        }

        user = new User();
        BeanMapper.copy(userDto, user);
        user.setIsDefault(false);
        user.setPassword(password);

        encryptPassword(user);

        logger.debug("Saving user:{}", userDto);
        userDao.save(user);
        logger.info("Save done, user={}", user.getLoginName());

        if ("on".equals(userDto.getIsSendEmail())) {
            MailDto mailDTO = new MailDto();
            mailDTO.setSubject("Account Registration successful");
            mailDTO.setContent("Your account Registration successful!<br>loginName is : " + userDto.getLoginName()+"<br>password is : " + password);
            mailDTO.setUsingDefault(false);
            mailDTO.setTargetAddress(userDto.getEmail());
            mailDTO.setNickname(userDto.getLoginName());

            mimeMailService.sendMailAsynchronously(mailDTO);

        }
    }

    @Override
    public void updateUser(UserDto userDto) {
        if (userDto == null || userDto.getId() == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        User user = userDao.findById(userDto.getId());
        BeanUtil.copyBeanPropertiesWithoutNull(userDto, user);

        logger.debug("Updating user:{}", userDto);
        userDao.save(user);
        logger.info("Update done, user={}", user.getLoginName());
    }

    @Override
    public void deleteUser(String id) {
        if (id == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        Long idL = Long.valueOf(id);
        User user = userDao.findById(idL);
        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }
        Boolean isDefault = user.getIsDefault();
        if (isDefault == false) {
            user.getRoles().clear();
            logger.debug("Deleting user:{}", user.getLoginName());
            userDao.delete(user);
            logger.info("Delete done, user={}", user.getLoginName());
        }
    }

    @Override
    public UserDto findById(String id) {
        if (id == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        User user = userDao.findById(Long.valueOf(id));

        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        UserDto userDto = new UserDto();
        BeanMapper.copy(user, userDto);

        return userDto;
    }

    @Override
    public List<RoleDto> findUserCanAddRoles(String uuid) {
        if (uuid == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findById(Long.valueOf(uuid));
        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }
        List<Role> roleList = (List<Role>) roleDao.findAll();
        for (Role role : user.getRoles()) {
            if (roleList.contains(role)) {
                roleList.remove(role);
            }
        }
        List<RoleDto> roleDtos = Lists.newArrayList();
        for (Role role : roleList) {

            RoleDto roleDto = new RoleDto();
            BeanMapper.copy(role, roleDto);

            if (Constant.ROLE_DISABLE.equals(roleDto.getEnabled()))
                continue;

            roleDtos.add(roleDto);
        }
        return roleDtos;
    }

    @Override
    public UserDto findByLoginName(String name) {
        if (StringUtils.isBlank(name)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findByLoginName(name);

        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        UserDto userDto = new UserDto();
        BeanMapper.copy(user, userDto);

        return userDto;
    }

    @Override
    public List<UserDto> findAllUsers() {
        List<User> users = (List<User>) userDao.findAll();
        if (users == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }
        List<UserDto> userDtos = Lists.newArrayList();

        for (User user : users) {
            UserDto userDto = new UserDto();
            BeanMapper.copy(user, userDto);

            userDtos.add(userDto);
        }

        return userDtos;
    }

    @Override
    public List<UserDto> searchUser(Map<String, Object> searchParams) {
        logger.debug("Searching user with {},{}", searchParams.keySet(), searchParams.values());
        Map<String, SearchFilter> filters = SearchFilter.parse(searchParams);
        Specification<User> spec = DynamicSpecifications.bySearchFilter(filters.values(), User.class);

        List<User> users = userDao.findAll(spec);
        logger.debug("Searching user, total elements:{}", users.size());

        List<UserDto> userDtos = Lists.newArrayList();
        for (User user : users) {
            UserDto userDto = new UserDto();
            Hibernates.initLazyProperty(user.getRoles());

            BeanMapper.copy(user, userDto);
            userDtos.add(userDto);
        }

        return userDtos;
    }

    @Override
    public Page<UserDto> searchUser(Map<String, Object> searchParams, Pageable pageable) {
        logger.debug("Searching user with {},{},{}", searchParams.keySet(), searchParams.values(), pageable);

        Map<String, SearchFilter> filters = SearchFilter.parse(searchParams);
        Specification<User> spec = DynamicSpecifications.bySearchFilter(filters.values(), User.class);

        Page<User> userPage = userDao.findAll(spec, pageable);
        logger.debug("Searching user, total elements:{}", userPage.getTotalElements());
        List<User> users = userPage.getContent();
        List<UserDto> userDtos = Lists.newArrayList();
        for (User user : users) {
            UserDto userDto = new UserDto();
            BeanMapper.copy(user, userDto);
            userDtos.add(userDto);
        }

        Page<UserDto> pageDto = new PageImpl<UserDto>(userDtos, pageable, userPage.getTotalElements());

        return pageDto;
    }

    @Override
    public Page<UserDto> searchUserByOrg(String orgName, Pageable pageable) {
        logger.debug("Searching user with orgName {}", orgName, pageable);
        Specification<User> spec = new Specification<User>() {
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

                Join<User, Organization> orgJoin = root.join(root.getModel().getSingularAttribute("organization", Organization.class), JoinType.LEFT);
                Predicate p4 = cb.equal(orgJoin.get("name").as(String.class), orgName);
                query.where(p4);
                return query.getRestriction();
            }
        };


        Page<User> userPage = userDao.findAll(spec, pageable);
        logger.debug("Searching user, total elements:{}", userPage.getTotalElements());
        List<User> users = userPage.getContent();
        List<UserDto> userDtos = Lists.newArrayList();
        for (User user : users) {
            UserDto userDto = new UserDto();
            Hibernates.initLazyProperty(user.getRoles());

            BeanMapper.copy(user, userDto);
            userDtos.add(userDto);
        }

        Page<UserDto> pageDto = new PageImpl<UserDto>(userDtos, pageable, userPage.getTotalElements());

        return pageDto;
    }

    @Override
    /**
     * add assign roles
     */
    public void assignRolesForUser(String userId, List<String> roleIds) {
        if (userId == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findById(Long.valueOf(userId));

        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }
        Role role = new Role();
        for (int i = 0; i < roleIds.size(); i++) {
            String roleId = roleIds.get(i);
            if (roleId == null) {
                throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
            }
            role = roleDao.findById(Long.valueOf(roleId));

            user.getRoles().add(role);
        }
        userDao.save(user);
    }

    @Override
    public void deleteRolesForUser(String userId, List<String> roleIds) {
        if (userId == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findById(Long.valueOf(userId));
        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }
        Role role = new Role();
        for (int i = 0; i < roleIds.size(); i++) {
            String roleId = roleIds.get(i);
            role = roleDao.findById(Long.valueOf(roleId));

            user.getRoles().remove(role);
        }

        userDao.save(user);
    }

    @Override
    public List<RoleDto> findUserHaveRole(String userId) {
        if (userId == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findById(Long.valueOf(userId));
        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }
        List<RoleDto> roleDtos = Lists.newArrayList();
        for (Role role : user.getRoles()) {
            RoleDto roleDto = new RoleDto();
            BeanMapper.copy(role, roleDto);
            roleDtos.add(roleDto);
        }
        return roleDtos;
    }

    @Override
    public void resetPassword(String userId) {
        if (userId == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findById(Long.valueOf(userId));
        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        String newRandomPassword = getRandomString(systemConfig.getPasswordSize());
        user.setPassword(newRandomPassword);
        encryptPassword(user);

        MailDto mailDTO = new MailDto();
        mailDTO.setContent("Your new password： " + newRandomPassword + "<br>Please reset the new password login your account");
        mailDTO.setUsingDefault(false);
        mailDTO.setTargetAddress(user.getEmail());
        mailDTO.setNickname(user.getLoginName());

        mimeMailService.sendMailAsynchronously(mailDTO);


        userDao.save(user);
    }

    @Override
    public void updatePassword(String loginName, String password, String newPassword) {
        if (StringUtils.isBlank(loginName)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        User user = userDao.findByLoginName(loginName);

        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        byte[] accountCredentials = this.encryptPassword(password, user.getSalt());
        if (user.getPassword().equals(Encodes.encodeHex(accountCredentials))) {
            user.setPassword(newPassword);
            encryptPassword(user);
            userDao.save(user);
        } else {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_CREDENTIALS_FAIL);
        }

    }

    private byte[] encryptPassword(String password, String salt) {
        byte[] s = Encodes.decodeHex(salt);
        return Digests.sha1(password.getBytes(), s, SecurityService.HASH_INTERATIONS);
    }

    private void encryptPassword(User user) {
        byte[] salt = Digests.generateSalt(SecurityService.SALT_SIZE);
        user.setSalt(Encodes.encodeHex(salt));

        byte[] hashPassword = Digests.sha1(user.getPassword().getBytes(), salt, SecurityService.HASH_INTERATIONS);
        user.setPassword(Encodes.encodeHex(hashPassword));
    }


    private boolean equals(Object tokenCredentials, Object accountCredentials) {
        byte[] tokenBytes = this.toBytes(tokenCredentials);
        byte[] accountBytes = this.toBytes(accountCredentials);
        return Arrays.equals(tokenBytes, accountBytes);
    }

    private byte[] toBytes(Object o) {
        if (o == null) {
            String msg = "Argument for byte conversion cannot be null.";
            throw new IllegalArgumentException(msg);
        } else {
            return o instanceof byte[] ? (byte[]) ((byte[]) o) : (o instanceof ByteSource ? ((ByteSource) o).getBytes() : (o instanceof char[] ? toBytes((char[]) ((char[]) o)) : (o instanceof String ? toBytes((String) o) : (o instanceof File ? this.toBytes((File) o) : (o instanceof InputStream ? this.toBytes((InputStream) o) : this.objectToBytes(o))))));
        }
    }

    private byte[] objectToBytes(Object o) {
        String msg = "The " + this.getClass().getName() + " implementation only supports conversion to " + "byte[] if the source is of type byte[], char[], String, " + ByteSource.class.getName() + " File or InputStream.  The instance provided as a method " + "argument is of type [" + o.getClass().getName() + "].  If you would like to convert " + "this argument type to a byte[], you can 1) convert the argument to one of the supported types " + "yourself and then use that as the method argument or 2) subclass " + this.getClass().getName() + "and override the objectToBytes(Object o) method.";
        throw new CodecException(msg);
    }
}
